/******************************************************************************
 * SVN­Versioning-Information:
 * ­­­­­­­­­­­­­­­­­­­­­­­­
 * $LastChangedRevision: 6 $
 * $LastChangedBy: c.a.duevel $
 * $LastChangedDate: 2009-08-26 15:12:37 +0000 (Wed, 26 Aug 2009) $
 * $HeadURL: http://robofight.googlecode.com/svn/trunk/Server/src/de/hft_stuttgart/botwar/server/model/Game.java $
 ******************************************************************************/
package de.hft_stuttgart.botwar.server.model;

import de.hft_stuttgart.botwar.common.commands.InitGameCmd;
import de.hft_stuttgart.botwar.common.interfaces.IRemotePlayer.MODE;
import de.hft_stuttgart.botwar.common.models.GameInfo;
import de.hft_stuttgart.botwar.common.models.Position;
import de.hft_stuttgart.botwar.common.models.RobotInfo;
import de.hft_stuttgart.botwar.server.DataBase;
import de.hft_stuttgart.botwar.server.remote.RemotePlayer;
import java.rmi.RemoteException;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.Iterator;
import org.apache.log4j.Logger;
import java.util.Random;

/**
 * Represents a game.
 * @author c.a.duevel@gmail.com, sdegiorgi@gmail.com
 */
public class Game {

    static Logger logger = Logger.getLogger(Game.class);
    static Map<Integer, Game> games = new HashMap<Integer, Game>(); //all games
    static Map<Integer, GameInfo> gameInfoMap = new HashMap<Integer, GameInfo>();
    static List<Integer> gamesWithoutPlayer = new ArrayList(10);
    static final Random R = new Random();
    RunningGame runningGame;
    List<RemotePlayer> players = new ArrayList<RemotePlayer>(4);
    Map<Integer, Robot> robots = new HashMap<Integer, Robot>();
    final Integer ID;
    private final GameInfo GAME_INFO;
    private boolean initializing = false;
    RemotePlayer gameOwner = null;
    String gameName;
    GameMap map;
    int maxPlayers = 0;
    int maxBots = 0;
    int maxPlayerLevel = 99;

     private Game(Integer ID, RemotePlayer player, String gameName,
            GameMap map, int maxPlayers, int maxBots,
            int maxPlayerLevel) {
        GAME_INFO = new GameInfo(ID, player.getID(), gameName,
               map.name, maxPlayers, maxBots, maxPlayerLevel);
        this.ID = ID;
        this.gameOwner = player;
        this.gameName = gameName;
        this.map = map;
        this.maxPlayers = maxPlayers;
        this.maxBots = maxBots;
        this.maxPlayerLevel = maxPlayerLevel;
        games.put(ID, this);
        gameInfoMap.put(ID, GAME_INFO);
    }

    //TODO: server generated game
    /**
     * should be for the server to create a "fast game"
     * @param ID
     * @deprecated not jet implemented
     */
    @Deprecated
     private Game(Integer ID, GameMap map) {
        GAME_INFO = new GameInfo(ID);
        this.ID = ID;
        this.gameOwner = null;
        this.gameName = "gameName";
        this.map = map;
        this.maxPlayers = 99;
        this.maxBots = 10;
        this.maxPlayerLevel = Coinsystem.getTopLevel();
        games.put(ID, this);
        gameInfoMap.put(ID, GAME_INFO);
    }
/**
 * Create a new game.
 * @param player The player who owns this game.
 * @param gameName The name of this game.
 * @param mapName The name of the map, on which the game is played.
 * @param maxPlayers The maxiumum number of players who are allowed in this game.
 * @param maxBots The maxiumum number of bots for this game.
 * @param maxPlayerLevel The maxiumum player level
 * @return
 */
    public static synchronized Integer createGame(RemotePlayer player,
            String gameName, String mapName, int maxPlayers, int maxBots,
            int maxPlayerLevel) {
        Integer id;
        for (Game game : games.values()) {
            if (game.gameOwner == player){
                throw new IllegalStateException("You has already created a game");
            }
        }
        GameMap map =GameMap.allMaps.get(mapName);
        if (map==null){
           throw new IllegalArgumentException(String.format("There is no" +
                   " map named %s!",mapName));
        }
        do {
            id = R.nextInt();
        } while (gameInfoMap.containsKey(id));
        new Game(id, player, gameName, map, maxPlayers, maxBots,
                maxPlayerLevel);
        return id;
    }

    //TODO: Zufallszahlen können sich wiederholen. Ungebrauchte Methode löschen.
    @Deprecated
    public static synchronized Integer createGame(RemotePlayer player) {
        Integer id;
        do {
            id = R.nextInt();
        } while (gameInfoMap.containsKey(id));
        new Game(id,null);
        return id;
    }

    public boolean isInitializing() {
        return initializing;
    }

    public static Game getGame(final Integer ID) {
        Game game = games.get(ID);
        if (game == null) {
            throw new IllegalArgumentException(String.format("There is no" +
                    " game with ID=%d", ID));
        }
        return game;
    }

    public static List<GameInfo> getGameInfos() {
        return new ArrayList<GameInfo>(gameInfoMap.values());
    }

    public static GameInfo getGameInfo(Integer gameID) {
        return gameInfoMap.get(gameID);
    }

    public Position getPosOfRobot(Integer ID) {
        Robot r = robots.get(ID);
        if (r == null){
            throw new IllegalArgumentException("Robot not in game!");
        }
        return r.getPos();
    }

    public void setMode(Integer RobotId, MODE mode, String usrID) {
        checkifGameIsStarted();
        robots.get(RobotId).setMode(mode, usrID);

    }

    /**
     * Returns the running state of this game.
     * @return true if the game is running, otherwise false
     */
    public boolean isRunning() {
        if (runningGame != null){
            return runningGame.isRunning();
        }
        return false;
    }

    public void setTarget(Integer RobotId, Position target, String ID) {
        checkifGameIsStarted();
        Robot r =robots.get(RobotId);
        if(r!=null){
            r.setUserTarget(target, ID);
        }
        else{
            throw new IllegalArgumentException("No robot with such an ID exists!");
        }
    }

    /**
     * Sets up the given robots from given player, according to the maxNum
     * allowed and the players preferencelist
     * @param player A reference to the remoteplayer
     * @param robotList A preference list of robot ids the player likes to use
     */
    public void setPlayerRobots(RemotePlayer player, List<Integer> robotList) throws RemoteException {
        if (isRunning()) {
            throw new IllegalStateException("Game has already started - can't " +
                    "setup robots.");
        }
        if (isInitializing()) {
            throw new IllegalStateException("Game is initializing - can't get" +
                    "any more robots!");

        }
        if(!players.contains(player)){
            throw new IllegalArgumentException("This player isn't in this" +
                    " game!");
        }

        long cost = 0;
        List<RobotConfiguration> list = new ArrayList<RobotConfiguration>(maxBots);
        for(Integer id : robotList) {
            RobotConfiguration dbRobot = DataBase.get().getRobot(id);
            cost += dbRobot.getCost();
            //System.out.println(cost);

            //player can only add own robots
            if (dbRobot.getPlayer().getUsername().equals(player.getID())){
                list.add(dbRobot);
            }

            //player can only add robots with the level less or equals his own level.
            if (dbRobot.getLevelRank() > player.getLevelRank()){
                throw new IllegalArgumentException("You need a higher level for this robot.");
            }

            //player can only add robots with the level less or equals the maxPlayerLevel
            if (dbRobot.getLevelRank() > this.getMaxPlayerLevel()){
                throw new IllegalArgumentException("Level of robot not allowed.");
            }
        }

        //if the max. number of robots is reached, break
        if (list.size() > maxBots){
            throw new IllegalArgumentException("Too many robots!");
         }

        //if there are no robots insert
        if (list.size() == 0){
            throw new IllegalArgumentException("No robot selected!");
        }

        //if the robotteam is to expensive for this game
        if (cost >= Coinsystem.getCoinsForLevel(maxPlayerLevel+1)){
            throw new IllegalArgumentException("Too expensive robot(s) for this game!");
        }

        //if the robotteam is to expensive for the user
        if (cost > player.getCoins()){
            throw new IllegalArgumentException("You have not enough coins!");
        }

        for (RobotConfiguration dbRobot : list) {
           Robot r = new Robot(dbRobot,
                   map.getStartPoints().get(robots.size()));
          robots.put(r.getID(), r);
          GAME_INFO.addRobot(player.getID(), dbRobot.getInfo());
        }

    }

    /**
     * Checks if all players in a game are ready to go
     * @return
     */
    public boolean start() {
        if (!isInitializing()) {
            throw new IllegalStateException("Game is not initialized yet!");
        }
        if (players.size()<2){
            throw new IllegalStateException("There must be at least two " +
                    "players in a game to start!");
        }
        for (RemotePlayer p : players) {
            if (!p.isReady()) {
                return false;
            }
        }
        assert runningGame == null;
        GAME_INFO.running = true;
        initializing=false;
        runningGame = new RunningGame(this);
        logger.info("A new game was started!");

        return true;
    }

    public void init(String userId) {
        if(GAME_INFO.countRobots(userId) == 0){
            throw new IllegalStateException("Player must have at least" +
                    " one robot to init a game.");
        }
        if(isRunning()){
            throw new IllegalStateException("Game has already started");
        }
        if (players.size()<2){
            return;
        }
        for (RemotePlayer player : players) {
            if (!player.isReadyToInitialize()) {
                return;
            }
        }
        initializing = true;
        for (RemotePlayer player : players) {
            player.addCommand(new InitGameCmd());
        }

    }

    public void unReady(RemotePlayer rPlayer) {
        for(RobotInfo robot : this.GAME_INFO.getRobotInfos(rPlayer.getID())) {
            this.robots.remove(robot.getRobotID());
        }
        this.GAME_INFO.removePlayer(rPlayer.getID());
    }

    Map<Integer, Robot> getRobots() {
        return robots;
    }

    public Iterable<RemotePlayer> getPlayers() {
        return players;
    }

    /**
     * Called if a player enters this game. The first player, who enters
     * the game will be the host. Normaly it will be the creater.
     * @param player the player who enters
     */
    public void addPlayer(RemotePlayer player) {
        if (isRunning()) {
            throw new UnsupportedOperationException("Can't add a player to a" +
                    " running game");
        } else if (isInitializing()) {
            throw new UnsupportedOperationException("Can't add a player to an" +
                    " initializing game.");
        } else {
            if (players.size() == 0){
                this.gameOwner = player;
            }
            players.add(player);
            GAME_INFO.addPlayer(player.getID());
        }
    }

    private void checkifGameIsStarted() {
        if (!this.isRunning()) {
            throw new IllegalStateException("Game is not yet started!");
        }
    }

    List<String> getPlayersStringList() {
        ArrayList playerList = new ArrayList();
        for (RemotePlayer player : this.getPlayers()) {
            playerList.add(player.getID());
        }
        return playerList;
    }

    public RemotePlayer getGameOwner() {
        return gameOwner;
    }

    public boolean isPlayerInGame(String playerID) {
        for (RemotePlayer p : players) {
            if (p.getID().equals(playerID)) {
                return true;
            }
        }
        return false;
    }

    public void removePlayer(RemotePlayer player) {
        if(players.contains(player)) {
            player.setGame(null);
            GAME_INFO.removePlayer(player.getID());
            players.remove(player);
            player.finishGame();
        }
    }

    public Integer getGameId() {
        return ID;
    }

    public int getMaxPlayers() {
        return maxPlayers;
    }

    public int getNumConnectedPlayers() {
        return players.size();
    }

    public void close() {
        gameInfoMap.remove(this.ID);
        games.remove(this.ID);
    }

    public int getMaxPlayerLevel() {
        return maxPlayerLevel;
    }


    /**
     * Checks all games for having a running game object but not to be running.
     * If there is such a game, it will be deleted.
     */
    public static void deleteCrashedGames(){
        List<Game> deleteGame = new ArrayList<Game>();
        for(Game game : games.values()){
            if (game.runningGame != null){
                if (!game.isRunning()){
                    deleteGame.add(game);
                }
            }
        }
        for (Game game : deleteGame) {
            logger.warn(String.format(
                    "game agressivly deleted after crash: %s (%d)",
                    game.gameName,
                    game.getGameId()));
            game.close();
        }
    }

    /**
     * Checks all games for having players inside and deletes those, who does
     * not have a player inside.
     */
    public static void deleteGamesWithoutPlayer() {
        List<Game> deleteGame = new ArrayList<Game>();

        for (Integer key : gameInfoMap.keySet()) {
            Game game = games.get(key);
            boolean found = false;

            //search for games without a host insid
            for (Iterator<RemotePlayer> it = game.getPlayers().iterator(); it.hasNext();) {
                RemotePlayer player = it.next();
                if (player.equals(game.getGameOwner())){
                    found = true;
                    break;
                }
            }

            if (found){
                //the game has a hoster insid
                gamesWithoutPlayer.remove(key);
            } else {
                //the game has no player insid
                if (!gamesWithoutPlayer.contains(key)){
                    //the player must have enough time between creating the game
                    //and entering it, so at first save, that there is a
                    //player missing.
                    gamesWithoutPlayer.add(key);
                } else {
                    //Second, if the player is not insered since two calles,
                    //the game will not get a player any more.
                    deleteGame.add(game);
                }
            }
        }

        for (Game game : deleteGame) {
            logger.info(String.format(
                    "empty game agressivly deleted: %s (%d)",
                    game.gameName,
                    game.getGameId()));
            game.close();
        }
    }

    @Override
    public String toString() {
        return this.gameName + "(id: " + this.ID +
                                ", owner: " + this.gameOwner.getID() +
                                ", maxBots: " + this.maxBots +
                                ", maxPlayers: " + this.maxPlayers +
                                ", maxPlayerLevel: " + this.maxPlayerLevel +
                                ", map: " + this.map.name +
                                ")";
    }
}

